package org.dmcs.homework.statistics.classifiers.implementations;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.dmcs.homework.statistics.classifiers.api.IClassDistanceCalculator;
import org.dmcs.homework.statistics.classifiers.api.IClassifier;
import org.dmcs.homework.statistics.classifiers.api.IStopCriterion;

/**
 * A generic hierarchical classifier with configurable distance measures and 
 * classification termination criteria.
 * @author Zielony
 * @version 1.0
 */
public class HierarchicalClassifier implements IClassifier {
	/**
	 * A tool to calculate inter-class distance.
	 */
	protected IClassDistanceCalculator classDistanceCalculator;
	
	/**
	 * A tool to decide whether (a at given point) the classification should stop.
	 */
	protected IStopCriterion stopCriterion;
	
	/**
	 * The calculated classes.
	 */
	protected Map<Integer, List<Integer>> classes;

	/**
	 * The data set.
	 */
	protected double[][] data;
	
	/**
	 * The considered features.
	 */
	protected int[] featureSet;
	
	public HierarchicalClassifier(IStopCriterion stopCriterion, 
			IClassDistanceCalculator classDistanceCalculator) {
		this.stopCriterion = stopCriterion;
		this.classDistanceCalculator = classDistanceCalculator;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int[][] classify(double[][] data, int[] featureSet) {
		/*
		 * Obsluga wyjatkow.
		 */
		if(data == null) {
			throw new NullPointerException("HierarchicalClassifier|classify|"
					+ "data array null");
		}
		else if(data.length == 0) {
			throw new IllegalArgumentException("HierarchicalClassifier|classify|"
					+ "data array is empty");
		}
		else if(data[0].length == 0) {
			throw new IllegalArgumentException("HierarchicalClassifier|classify|"
					+ "data array is empty");
		}
		
		if(featureSet == null) {
			throw new NullPointerException("HierarchicalClassifier|classify|"
					+ "feature set null");
		}
		else if(featureSet.length == 0) {
			throw new IllegalArgumentException("HierarchicalClassifier|classify|"
					+ "feature set is empty");
		}
		
		/*
		 * Inicjalizacja klas - kazdy punkt to osobna klasa.
		 */
		initializeClasses();
	
		/*
		 * Iteracje dopóki nie osigniemy warunku zatrzymania.
		 */
		while(stopCriterion.shouldStop(classes, data)) {
			/*
			 * Szukanie klas o najmniejszej odleglosci od siebie.
			 */
			int[] minimalDistanceClasses = getClassesWithMinimalDistance();
			
			/*
			 * Łaczenie klas o najmniejszej odleglosci od siebie.
			 */
			mergeClasses(minimalDistanceClasses[0], minimalDistanceClasses[1]);
		}
		
		/*
		 * Końcowa forma klasyfikacji.
		 */
		int[][] finalForm = new int[classes.size()][];
		
		/*
		 * Lista - klasa do przepisania.
		 */
		List<Integer> iList;
		
		/*
		 * Przepisywanie danych.
		 */
		for(int i=0; i< classes.size(); i++) {
			/*
			 * Odczyt i-tej klasy.
			 */
			iList = classes.get(i);
			
			finalForm[i] = new int[iList.size()];
			
			for(int j=0; j<iList.size(); j++) {
				/*
				 * Przepisywanie danych.
				 */
				finalForm[i][j] = iList.get(j);
			}
		}
		
		return finalForm;
	}
	
	/**
	 * Finds a pair of classes that are within minimal distance to eachother.
	 * @return the indices of the classes in an <code>int[2]</code> form.
	 */
	protected int[] getClassesWithMinimalDistance() {
		/*
		 * The first class in a checked pair.
		 */
		List<Integer> firstClass;
		
		/*
		 * The second class in a checked pair.
		 */
		List<Integer> secondClass;
		
		/*
		 * The index of the first class from the minimal distance pair in the
		 * classes Map.
		 */
		int firstMergedIndex = -1;
		
		/*
		 * The index of the second class from the minimal distance pair in the
		 * classes Map.
		 */
		int secondMergedIndex = -1;
		
		/*
		 * The minimal distance between classes. Initialized with the highest possible
		 * value.
		 */
		double minimalClassDistance = Double.POSITIVE_INFINITY;
		
		/*
		 * The distance between the i-th and j-th class.
		 */
		double iDistance;
		
		/*
		 * Checking all the class pairs.
		 */
		for(int i=0; i< classes.size(); i++) {
			/*
			 * Fetching the first class.
			 */
			firstClass = classes.get(i);
			
			for(int j=0; j< classes.size(); j++) {
				/*
				 * The distance from the class to itself is not fair game.
				 */
				if( i == j) {
					continue;
				}
				
				/*
				 * Fetching the second class.
				 */
				secondClass = classes.get(i);
				
				/*
				 * Calculating the distance using classDistanceCalculator.
				 */
				iDistance = classDistanceCalculator.calculateClassDistance(firstClass,
						secondClass, featureSet, data);
				
				/*
				 * If the new distance is lesser than the previous minimum , substituting
				 * the minimum and memorizing the indices.
				 */
				if(iDistance < minimalClassDistance) {
					firstMergedIndex = i;
					secondMergedIndex = j;
					
					minimalClassDistance = iDistance;
				}
			}
		}
		
		return (new int[]{firstMergedIndex, secondMergedIndex});
	}
	
	/**
	 * Merges the two classes into a new one.
	 * @param firstClassIndex the index of the first class to merge.
	 * @param secondClassIndex the index of the second class to merge.
	 */
	protected void mergeClasses(int firstClassIndex, int secondClassIndex) {
		/*
		 * Klasy laczone z jedna.
		 */
		List<Integer> firstClass = classes.get(firstClassIndex);
		List<Integer> secondClass = classes.get(secondClassIndex);
		
		/*
		 * Laczenie klas.
		 */
		classes.remove(secondClassIndex);
		secondClass.addAll(firstClass);
	}

	/**
	 * Creates the initial class set where each points constitutes a separate class.
	 */
	protected void initializeClasses() {
		/*
		 * i-ta poczatkowa klasa.
		 */
		List<Integer> iClass;
		
		for(int i=0; i < data[0].length; i++){
			iClass = new LinkedList<Integer>();
			
			/*
			 * Kazdy punkt stanowi osobna klase na poczatku.
			 */
			iClass.add(i);
			
			classes.put(i, iClass);
		}
	}
}